--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ae6d42d302b5a3ac7b7f5b7e7a1153beebae3017
Parents : 8bf7d35
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-16T21:49:53-05:00
fix(markdown): fix underscore handling for links, italic formatting and improve test coverage
Changes
2 files changed, 51 insertions(+), 6 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/js/MarkdownRenderer.js b/meshchatx/src/frontend/js/MarkdownRenderer.js
index 1c39d208..236da8fa 100644
--- a/meshchatx/src/frontend/js/MarkdownRenderer.js
+++ b/meshchatx/src/frontend/js/MarkdownRenderer.js
@@ -36,9 +36,9 @@ export default class MarkdownRenderer {
text = text.replace(/\*\*\*(.*?)\*\*\*/g, "<strong><em>$1</em></strong>");
text = text.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
text = text.replace(/\*(.*?)\*/g, "<em>$1</em>");
- text = text.replace(/___(.*?)___/g, "<strong><em>$1</em></strong>");
- text = text.replace(/__(.*?)__/g, "<strong>$1</strong>");
- text = text.replace(/_(.*?)_/g, "<em>$1</em>");
+ text = text.replace(/(^|[^\w])___(.*?)___(?=[^\w]|$)/g, "$1<strong><em>$2</em></strong>");
+ text = text.replace(/(^|[^\w])__(.*?)__(?=[^\w]|$)/g, "$1<strong>$2</strong>");
+ text = text.replace(/(^|[^\w])_(.*?)_(?=[^\w]|$)/g, "$1<em>$2</em>");
// Blockquotes
text = text.replace(
@@ -126,9 +126,9 @@ export default class MarkdownRenderer {
text = text.replace(/\*\*\*(.*?)\*\*\*/g, "$1");
text = text.replace(/\*\*(.*?)\*\*/g, "$1");
text = text.replace(/\*(.*?)\*/g, "$1");
- text = text.replace(/___(.*?)___/g, "$1");
- text = text.replace(/__(.*?)__/g, "$1");
- text = text.replace(/_(.*?)_/g, "$1");
+ text = text.replace(/(^|[^\w])___(.*?)___(?=[^\w]|$)/g, "$1$2");
+ text = text.replace(/(^|[^\w])__(.*?)__(?=[^\w]|$)/g, "$1$2");
+ text = text.replace(/(^|[^\w])_(.*?)_(?=[^\w]|$)/g, "$1$2");
// Strip inline code
text = text.replace(/`([^`]+)`/g, "$1");
diff --git a/tests/frontend/MarkdownRenderer.test.js b/tests/frontend/MarkdownRenderer.test.js
index c8804c67..b36cb042 100644
--- a/tests/frontend/MarkdownRenderer.test.js
+++ b/tests/frontend/MarkdownRenderer.test.js
@@ -26,6 +26,11 @@ describe("MarkdownRenderer.js", () => {
expect(result).toContain("<em>Italic</em>");
});
+ it("renders underscore italic when delimiters are word boundaries", () => {
+ const result = MarkdownRenderer.render("this is _italic_ text");
+ expect(result).toContain("this is <em>italic</em> text");
+ });
+
it("renders bold and italic text correctly", () => {
const result = MarkdownRenderer.render("***Bold and Italic***");
expect(result).toContain("<strong><em>Bold and Italic</em></strong>");
@@ -43,6 +48,16 @@ describe("MarkdownRenderer.js", () => {
expect(result).toContain("code");
});
+ it("keeps underscores intact in long https links", () => {
+ const url =
+ "https://git.quad4.io/RNS-Things/MeshChatX/src/branch/dev/docs/meshchatx_on_raspberry_pi.md";
+ const result = MarkdownRenderer.render(`visit ${url}`);
+ expect(result).toContain(`href="${url}"`);
+ expect(result).toContain(url);
+ expect(result).not.toContain("<em>on</em>");
+ expect(result).not.toContain("<em>raspberry</em>");
+ });
+
it("renders fenced code blocks correctly", () => {
const result = MarkdownRenderer.render("```python\nprint('hello')\n```");
expect(result).toContain("<pre");
@@ -57,6 +72,22 @@ describe("MarkdownRenderer.js", () => {
expect(result).toContain("Para 1");
expect(result).toContain("Para 2");
});
+
+ it("does not treat intraword underscores as italic markdown", () => {
+ const result = MarkdownRenderer.render("snake_case_identifier should remain plain");
+ expect(result).toContain("snake_case_identifier should remain plain");
+ expect(result).not.toContain("<em>case</em>");
+ });
+
+ it("keeps underscore-heavy urls intact while still rendering links", () => {
+ const url =
+ "https://example.com/docs/meshchatx_on_raspberry_pi.md?file=meshchatx_on_raspberry_pi.md#meshchatx_on_raspberry_pi";
+ const result = MarkdownRenderer.render(`see ${url} now`);
+ expect(result).toContain(`href="${url}"`);
+ expect(result).toContain(url);
+ expect(result).not.toContain("<em>on</em>");
+ expect(result).not.toContain("<em>raspberry</em>");
+ });
});
describe("security: XSS prevention", () => {
@@ -256,6 +287,14 @@ describe("MarkdownRenderer.js", () => {
expect(stripped).not.toContain("` ");
});
+ it("strip removes underscore italics but keeps intraword underscores", () => {
+ const input = "before _italic_ after and snake_case_word";
+ const stripped = MarkdownRenderer.strip(input);
+ expect(stripped).toContain("before italic after");
+ expect(stripped).toContain("snake_case_word");
+ expect(stripped).not.toContain("_italic_");
+ });
+
it("strip handles null and undefined without throwing", () => {
expect(MarkdownRenderer.strip(null)).toBe("");
expect(MarkdownRenderer.strip(undefined)).toBe("");
@@ -269,6 +308,12 @@ describe("MarkdownRenderer.js", () => {
expect(Date.now() - start).toBeLessThan(200);
});
+ it("strip keeps intraword underscores intact", () => {
+ const url =
+ "https://git.quad4.io/RNS-Things/MeshChatX/src/branch/dev/docs/meshchatx_on_raspberry_pi.md";
+ expect(MarkdownRenderer.strip(url)).toBe(url);
+ });
+
it("strip returns string for malformed and edge input", () => {
const edge = ["**no close", "```\ncode", "", " ", "\n\n", "[[CB0]] literal"];
edge.forEach((s) => {
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────